using System.Threading.Tasks;

namespace System.IO.Abstractions.TestingHelpers.Tests
{
    using Collections.Generic;

    using NUnit.Framework;

    using XFS = MockUnixSupport;

    public class MockFileOpenTests
    {
        [Test]
        public void MockFile_Open_ThrowsOnCreateNewWithExistingFile()
        {
            string filepath = XFS.Path(@"c:\something\already\exists.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            Assert.Throws<IOException>(() => filesystem.File.Open(filepath, FileMode.CreateNew));
        }

        [Test]
        public void MockFile_Open_ThrowsOnOpenWithMissingFile()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());

            Assert.Throws<FileNotFoundException>(() => filesystem.File.Open(filepath, FileMode.Open));
        }

        [Test]
        public void MockFile_Open_ThrowsOnTruncateWithMissingFile()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());

            Assert.Throws<FileNotFoundException>(() => filesystem.File.Open(filepath, FileMode.Truncate));
        }

        [Test]
        public void MockFile_Open_CreatesNewFileFileOnCreate()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
            filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

            var stream = filesystem.File.Open(filepath, FileMode.Create);

            Assert.That(filesystem.File.Exists(filepath), Is.True);
            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(0));
        }

        [Test]
        public void MockFile_Open_AllowsReadWriteOnCreate()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
            filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

            var stream = filesystem.File.Open(filepath, FileMode.Create);

            Assert.That(stream.CanRead, Is.True);
            Assert.That(stream.CanWrite, Is.True);
        }

        [Test]
        public void MockFile_Open_CreatesNewFileFileOnCreateNew()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
            filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

            var stream = filesystem.File.Open(filepath, FileMode.CreateNew);

            Assert.That(filesystem.File.Exists(filepath), Is.True);
            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(0));
        }

        [Test]
        public void MockFile_Open_OpensExistingFileOnAppend()
        {
            string filepath = XFS.Path(@"c:\something\does\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            var stream = filesystem.File.Open(filepath, FileMode.Append);
            var file = filesystem.GetFile(filepath);

            Assert.That(stream.Position, Is.EqualTo(file.Contents.Length));
            Assert.That(stream.Length, Is.EqualTo(file.Contents.Length));
        }

        [Test]
        public void MockFile_Open_OpensExistingFileOnTruncate()
        {
            string filepath = XFS.Path(@"c:\something\does\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            var stream = filesystem.File.Open(filepath, FileMode.Truncate);
            var file = filesystem.GetFile(filepath);

            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(0));
            Assert.That(file.Contents.Length, Is.EqualTo(0));
        }

        [Test]
        public void MockFile_Open_OpensExistingFileOnOpen()
        {
            string filepath = XFS.Path(@"c:\something\does\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            var stream = filesystem.File.Open(filepath, FileMode.Open);
            var file = filesystem.GetFile(filepath);

            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(file.Contents.Length));

            byte[] data;
            using (var br = new BinaryReader(stream))
                data = br.ReadBytes((int)stream.Length);

            Assert.That(data, Is.EqualTo(file.Contents));
        }

        [Test]
        public void MockFile_Open_OpensExistingFileOnOpenOrCreate()
        {
            string filepath = XFS.Path(@"c:\something\does\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            var stream = filesystem.File.Open(filepath, FileMode.OpenOrCreate);
            var file = filesystem.GetFile(filepath);

            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(file.Contents.Length));

            byte[] data;
            using (var br = new BinaryReader(stream))
                data = br.ReadBytes((int)stream.Length);

            Assert.That(data, Is.EqualTo(file.Contents));
        }

        [Test]
        public void MockFile_Open_CreatesNewFileOnOpenOrCreate()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>());
            filesystem.AddDirectory(XFS.Path(@"c:\something\doesnt"));

            var stream = filesystem.File.Open(filepath, FileMode.OpenOrCreate);

            Assert.That(filesystem.File.Exists(filepath), Is.True);
            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(0));
        }

        [Test]
        public void MockFile_Open_OverwritesExistingFileOnCreate()
        {
            string filepath = XFS.Path(@"c:\something\doesnt\exist.txt");
            var filesystem = new MockFileSystem(new Dictionary<string, MockFileData>
            {
                { filepath, new MockFileData("I'm here") }
            });

            var stream = filesystem.File.Open(filepath, FileMode.Create);
            var file = filesystem.GetFile(filepath);

            Assert.That(stream.Position, Is.EqualTo(0));
            Assert.That(stream.Length, Is.EqualTo(0));
            Assert.That(file.Contents.Length, Is.EqualTo(0));
        }

        [Test]
        public void MockFile_OpenText_ShouldRetainLastWriteTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            string filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(@"I'm here");
            var lastWriteTime = new DateTime(2012, 03, 21);
            file.LastWriteTime = lastWriteTime;
            fs.AddFile(filepath, file);

            // Act
            using (var reader = fs.File.OpenText(filepath))
            {
                reader.ReadLine();
            }

            // Assert
            Assert.That(fs.FileInfo.New(filepath).LastWriteTime, Is.EqualTo(lastWriteTime));
        }

        [Test]
        public void MockFile_OpenText_ShouldUpdateLastAccessTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            string filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(@"I'm here");
            var lastAccessTime = new DateTime(2012, 03, 21);
            file.LastAccessTime = lastAccessTime;
            fs.AddFile(filepath, file);

            // Act
            using (var reader = fs.File.OpenText(filepath))
            {
                reader.ReadLine();
            }

            // Assert
            Assert.That(DateTime.Now - fs.FileInfo.New(filepath).LastAccessTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
        }

        [Test]
        public void MockFile_Read_ShouldRetainCreationTimeAndUpdateLastAccessTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            var filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(new byte[] { 1, 2, 3 });
            var lastAccessTime = new DateTime(2012, 03, 21);
            file.LastAccessTime = lastAccessTime;
            var creationTime = new DateTime(2012, 03, 20);
            file.CreationTime = creationTime;
            fs.AddFile(filepath, file);

            var fi = fs.FileInfo.New(filepath);
            var stream = fi.OpenRead();
            var buffer = new byte[16];
            stream.Read(buffer, 0, buffer.Length);
            fi.Refresh();
            // Assert
            Assert.That(fi.CreationTime, Is.EqualTo(creationTime));
            Assert.That(DateTime.Now - fi.LastAccessTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
        }

        [Test]
        public async Task MockFile_ReadAsync_ShouldRetainCreationTimeAndUpdateLastAccessTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            var filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(new byte[] { 1, 2, 3 });
            var lastAccessTime = new DateTime(2012, 03, 21);
            file.LastAccessTime = lastAccessTime;
            var creationTime = new DateTime(2012, 03, 20);
            file.CreationTime = creationTime;
            fs.AddFile(filepath, file);

            var fi = fs.FileInfo.New(filepath);
            var stream = fi.OpenRead();
            var buffer = new byte[16];
            await stream.ReadAsync(buffer, 0, buffer.Length);
            fi.Refresh();
            // Assert
            Assert.That(fi.CreationTime, Is.EqualTo(creationTime));
            Assert.That(DateTime.Now - fi.LastAccessTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
        }

        [Test]
        public void MockFile_Write_ShouldRetainCreationTimeAndUpdateLastAccessTimeAndLastWriteTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            var filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(new byte[] { 1, 2, 3 });
            var lastAccessTime = new DateTime(2012, 03, 21);
            file.LastAccessTime = lastAccessTime;
            var creationTime = new DateTime(2012, 03, 20);
            file.CreationTime = creationTime;
            fs.AddFile(filepath, file);

            var fi = fs.FileInfo.New(filepath);
            var stream = fi.OpenWrite();
            var buffer = new byte[16];
            stream.Write(buffer, 0, buffer.Length);
            fi.Refresh();
            // Assert
            Assert.That(fi.CreationTime, Is.EqualTo(creationTime));
            Assert.That(DateTime.Now - fi.LastAccessTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
            Assert.That(DateTime.Now - fi.LastWriteTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
        }

        [Test]
        public async Task MockFile_WriteAsync_ShouldRetainCreationTimeAndUpdateLastAccessTimeAndLastWriteTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            var filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(new byte[] { 1, 2, 3 });
            var lastAccessTime = new DateTime(2012, 03, 21);
            file.LastAccessTime = lastAccessTime;
            var creationTime = new DateTime(2012, 03, 20);
            file.CreationTime = creationTime;
            fs.AddFile(filepath, file);

            var fi = fs.FileInfo.New(filepath);
            var stream = fi.OpenWrite();
            var buffer = new byte[16];
            await stream.WriteAsync(buffer, 0, buffer.Length);
            fi.Refresh();
            // Assert
            Assert.That(fi.CreationTime, Is.EqualTo(creationTime));
            Assert.That(DateTime.Now - fi.LastAccessTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
            Assert.That(DateTime.Now - fi.LastWriteTime, Is.LessThanOrEqualTo(TimeSpan.FromSeconds(1)));
        }

        [Test]
        public void MockFile_OpenText_ShouldRetainCreationTime()
        {
            // Arrange
            var fs = new MockFileSystem();
            string filepath = XFS.Path(@"C:\TestData\test.txt");
            var file = new MockFileData(@"I'm here");
            var creationTime = new DateTime(2012, 03, 21);
            file.CreationTime = creationTime;
            fs.AddFile(filepath, file);

            // Act
            using (var reader = fs.File.OpenText(filepath))
            {
                reader.ReadLine();
            }

            // Assert
            Assert.That(fs.FileInfo.New(filepath).CreationTime, Is.EqualTo(creationTime));
        }

        [Test]
        public void MockFile_Open_ShouldThrowDirectoryNotFoundExceptionIfFileModeCreateAndParentPathDoesNotExist()
        {
            // Arrange
            var fileSystem = new MockFileSystem();
            var file = XFS.Path("C:\\path\\NotFound.ext");

            // Act
            TestDelegate action = () => fileSystem.File.Open(file, FileMode.Create);

            // Assert
            var exception = Assert.Throws<DirectoryNotFoundException>(action);
            Assert.That(exception.Message, Does.StartWith("Could not find a part of the path"));
        }

        [Test]
        public void MockFile_OpenWrite_ShouldWorkWithRelativePath()
        {
            var file = "file.txt";
            var fileSystem = new MockFileSystem();

            fileSystem.File.OpenWrite(file).Close();

            Assert.That(fileSystem.File.Exists(file));
        }
    }
}